﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace LaTeXGrapher
{
    public class FunctionParser
    {
        enum State
        {
            None,
            SawKeyword,
            InHiddenExpressionAssignment,
            InExpressionAssignment,
            InExpressionAssignmentArguments,
            InExpressionAssignmentArgumentEndOrComma,
            InExpressionAssignmentWaitingForEquals,
            InExpression,
            InCondition,
            InSpecial,
            WaitForExpressionOrSpecial,
            Exit
        }

        Dictionary<string, Action<string>> _keywords = new Dictionary<string,Action<string>>();
        Tokenizer _lex;
        Action<string> _activeParser = null;

        // temp data
        string _currentName;
        List<string> _currentArguments = new List<string>();
        List<KeyValuePair<string, string>> _currentExpressions = new List<KeyValuePair<string,string>>();
        List<string> _currentSpecials = new List<string>();
        StringBuilder _currentExpression = new StringBuilder();
        StringBuilder _currentCondition = new StringBuilder();

        // Final data
        List<string> _plotFunctions = new List<string>();
        List<FunctionData> _functions = new List<FunctionData>();
        List<KeyValuePair<string,string>> _view = new List<KeyValuePair<string,string>>();
        List<LineData> _lines = new List<LineData>();
        List<PointData> _points = new List<PointData>();
        List<ShadeData> _shades = new List<ShadeData>();

        public FunctionParser(IEnumerable<char> source)
        {
            _keywords.Add("View", s => SetView(s));
            _keywords.Add("Line", s => AddLine(s));
            _keywords.Add("Segment", s => AddLineSegment(s));
            _keywords.Add("Tangent", s => AddTangent(s));
            _keywords.Add("Secant", s => AddSecant(s));
            _keywords.Add("Point", s => AddPoint(s));
            _keywords.Add("Label", s => AddLabel(s));
            _keywords.Add("Shade", s => AddShade(s));

            _lex = new Tokenizer(source);
        }

        public void LoadContext(FunctionContext context)
        {
            context.LoadFunctions(_functions);
            context.SetView(_view);
            context.SetLines(_lines);
            context.SetPoints(_points);
            context.SetShades(_shades);
            context.SetPlotFunctions(_functions.Where(f => _plotFunctions.Contains(f.Name)).Select(f => f.LoadContext(context)));
        }

        public void Parse()
        {
            State state = State.None;
            while (state != State.Exit)
                state = HandleParse(state);

            FinishFunction();
        }

        State HandleParse(State state)
        {
            switch (state)
            {
                default:
                    throw new Exception("Unexpected state \"" + state.ToString() + "\".");
                case State.None:
                    return HandleNone();
                case State.SawKeyword:
                    return HandleSawKeyword();
                case State.InHiddenExpressionAssignment:
                    return HandleInHiddenExpressionAssignment();
                case State.InExpressionAssignment:
                    return HandleInExpressionAssignment();
                case State.InExpressionAssignmentArguments:
                    return HandleInExpressionAssignmentArguments();
                case State.InExpressionAssignmentArgumentEndOrComma:
                    return HandleInExpressionAssignmentArgumentEndOrComma();
                case State.InExpressionAssignmentWaitingForEquals:
                    return SkipToToken("=", State.InExpression);
                case State.WaitForExpressionOrSpecial:
                    return HandleWaitForExpressionOrSpecial();
                case State.InExpression:
                    return HandleInExpression();
                case State.InCondition:
                    return HandleInCondition();
                case State.InSpecial:
                    return HandleInSpecial();
                case State.Exit:
                    return State.Exit;
            }
        }

        State SkipToToken(string data, State returnState)
        {
            for (; ; )
            {
                Token t = GetToken(data);
                if (t.Data == data)
                    return returnState;
            }
        }

        State HandleNone()
        {
            Token t = _lex.GetToken();
            if (t == null)
                return State.Exit; // end of input.

            switch (t.TokenType)
            {
                case TokenType.Identifier:
                    if (IsKeyword(t.Data))
                    {
                        _activeParser = _keywords[t.Data];
                        return State.SawKeyword;
                    }
                    BeginFunction(t.Data);
                    return State.InExpressionAssignment; // start of an expression.
                case TokenType.Operator:
                    if (t.Data == ">")
                    {
                        BeginHiddenFunction();
                        return State.InHiddenExpressionAssignment;
                    }
                    throw Unexpected(t, "identifier or keyword"); // start of an expression.
            }
            return State.None;
        }

        State HandleSawKeyword()
        {
            Debug.Assert(_activeParser != null);
            _activeParser(_lex.GetUntil('\n'));
            return State.None;
        }

        Token GetToken(string expectingMessage)
        {
            Token t = _lex.GetToken();
            if (t == null)
                throw Unexpected(t, expectingMessage);
            return t;
        }

        State HandleInExpression()
        {
            Token t = GetToken("expression");
            switch (t.TokenType)
            {
                case TokenType.Operator:
                    if (t.Data == "|")
                        return State.InCondition;
                    else if (t.Data == ":")
                        return State.InSpecial;
                    break;
                case TokenType.EndOfLine:
                    return State.None;
            }
            _currentExpression.Append(t.Data);
            return State.InExpression;
        }

        State HandleInHiddenExpressionAssignment()
        {
            Token t = GetToken("identifier");
            switch (t.TokenType)
            {
                case TokenType.Identifier:
                    _currentName = t.Data;
                    return State.InExpressionAssignment;
                case TokenType.WhiteSpace:
                    return State.InHiddenExpressionAssignment;
                default:
                    throw Unexpected(t, "identifier");
            }
        }

        State HandleInExpressionAssignment()
        {// saw the name looking for arguments or equals
            Token t = GetToken("arguments or \"=\"");
            switch (t.TokenType)
            {
                case TokenType.Operator:
                    if (t.Data == "(")
                        return State.InExpressionAssignmentArguments;
                    else if (t.Data == "=")
                        return State.InExpression;
                    break;
                case TokenType.WhiteSpace:
                    return State.InExpressionAssignment;
            }
            throw Unexpected(t, "arguments or \"=\"");
        }

        State HandleInExpressionAssignmentArguments()
        {
            Token t = GetToken("argument");
            switch (t.TokenType)
            {
                case TokenType.Identifier:
                    _currentArguments.Add(t.Data);
                    return State.InExpressionAssignmentArgumentEndOrComma;
                case TokenType.WhiteSpace:
                    return State.InExpressionAssignmentArguments;
                default:
                    throw Unexpected(t, "argument");
            }
        }

        State HandleInExpressionAssignmentArgumentEndOrComma()
        {
            Token t = GetToken("',' or ')'");
            switch (t.TokenType)
            {
                case TokenType.Operator:
                    if (t.Data == ")")
                        return State.InExpressionAssignmentWaitingForEquals;
                    else if (t.Data == ",")
                        return State.InExpressionAssignmentArguments;
                    break;
                case TokenType.WhiteSpace:
                    return State.InExpressionAssignmentArgumentEndOrComma;
            }
            throw Unexpected(t, "',' or ')'");
        }   

        State HandleInCondition()
        {
            Token t = GetToken("condition");
            switch (t.TokenType)
            {
                case TokenType.Operator:
                    if (t.Data == ":")
                        return State.InSpecial;
                    break;
                case TokenType.EndOfLine:
                    return State.WaitForExpressionOrSpecial;
            }
            _currentCondition.Append(t.Data);
            return State.InCondition;
        }

        State HandleInSpecial()
        {
            ParseSpecial(_lex.GetUntil('\n'));
            return State.None;
        }

        State HandleWaitForExpressionOrSpecial()
        {
            Token t = _lex.GetToken();
            if (t == null)
                return State.Exit; // end of input.

            switch (t.TokenType)
            {
                case TokenType.Identifier:
                    if (IsKeyword(t.Data))
                    {
                        _activeParser = _keywords[t.Data];
                        return State.SawKeyword;
                    }
                    BeginFunction(t.Data);
                    return State.InExpressionAssignment; // start of an expression.
                case TokenType.Operator:
                    if (t.Data == ">")
                    {
                        BeginHiddenFunction();
                        return State.InHiddenExpressionAssignment;
                    }
                    throw Unexpected(t, "identifier or keyword"); // start of an expression.
                case TokenType.WhiteSpace:
                    if (WhiteSpaceLength(t.Data) > 4)
                    {
                        FinishExpression();
                        return State.InExpression;
                    }
                    break;
            }
            return State.None;
        }

        static int WhiteSpaceLength(string str)
        {
            return ((IEnumerable<char>)str).Aggregate(0, (v, c) => v + ((c == '\t') ? 4 : 1));
        }

        void BeginHiddenFunction()
        {
            FinishFunction();
        }

        void BeginFunction(string start)
        {
            FinishFunction();

            _currentName = start;
            _plotFunctions.Add(start);
        }

        void FinishFunction()
        {
            if (_currentName == null)
                return;

            FinishExpression();

            _functions.Add(new FunctionData(_currentName, _currentArguments, _currentExpressions, _currentSpecials));

            _currentName = null;
            _currentExpressions.Clear();
            _currentArguments.Clear();
            _currentSpecials.Clear();
        }

        void FinishExpression()
        {
            if (_currentExpression.Length > 0)
            {
                _currentExpressions.Add(new KeyValuePair<string, string>(_currentExpression.ToString(), _currentCondition.ToString()));
                _currentExpression = new StringBuilder();
            }
            if (_currentCondition.Length > 0)
                _currentCondition = new StringBuilder();
        }

        // Markup
        void SetView(string data)
        {
            _view.AddRange(ParseListOfPairs(data));
        }

        void AddLine(string data)
        {
            _lines.Add(LineData.Create(ParseListOfPairs(data)));
        }

        void AddLineSegment(string data)
        {
            _lines.Add(LineData.CreateSegment(ParseListOfPairs(data)));
        }

        void AddTangent(string data)
        {
            _lines.Add(LineData.CreateTangent(ParseListOfNestedParens(data)));
        }

        void AddSecant(string data)
        {
            _lines.Add(LineData.CreateSecant(ParseListOfNestedParens(data)));
        }

        void AddPoint(string data)
        {
            _points.Add(PointData.Create(ParseListOfPairs(data)));
        }

        void AddLabel(string data)
        {
        }

        void AddShade(string data)
        {
            _shades.Add(ShadeData.Create(ParseListOfNestedParens(data)));
        }

        bool IsKeyword(string value)
        {
            return _keywords.ContainsKey(value);
        }

        Exception Unexpected(Token t, string expected)
        {
            if (t == null)
                return new Exception(string.Format("Unexpected end of input while looking for {0}.", expected));
            return new Exception(string.Format("Unexpected {0} \"{1}\" while looking for {2}.", t.TokenType, t.Data, expected));
        }

        Exception ParseFail(string data, string exptected, string parserName)
        {
            return new Exception(string.Format("Failed to with {0} parser, exptected {1}:\n{2}", parserName, exptected, data));
        }

        // Additional Parsers
        void ParseSpecial(string data)
        {
            var eqSplit = data.Split('=');
            if (eqSplit.Length != 2)
                throw ParseFail(data, "\"=\"", "special");
            var name = eqSplit[0].Trim();
            if (!Tokenizer.IsIdentifier(name))
                throw ParseFail(data, "identifier", "special");

            _currentSpecials.AddRange(ParseListOfNestedParens(eqSplit[1]).Select(s => s.Trim()));
        }

        public static IEnumerable<KeyValuePair<string, string>> ParseListOfPairs(string data)
        {
            foreach (var pair in ParseListOfNestedParens(data))
            {
                var s = pair.Trim();
                if (s.Length < 5)
                    throw new Exception("Failed to parse pair: " + pair);
                if (s[0] != '(')
                    throw new Exception("expected '(' to start pair: " + pair);
                if (s[s.Length - 1] != ')')
                    throw new Exception("expected ')' to end pair: " + pair);
                s = s.Substring(1, s.Length - 2);
                var array = ParseListOfNestedParens(s).ToArray();
                if (array.Length != 2)
                    throw new Exception("Failed to parse list of pairs: " + data);

                yield return new KeyValuePair<string, string>(array[0], array[1]);
            }
        }

        public static IEnumerable<string> ParseListOfNestedParens(string data)
        {
            StringBuilder sb = new StringBuilder();
            int nestCount = 0;
            foreach (char c in data)
            {
                switch (c)
                {
                    case ',':
                        if (nestCount == 0)
                        {
                            yield return sb.ToString();
                            sb = new StringBuilder();
                            continue;
                        }
                        break;
                    case ')':
                        nestCount--;
                        break;
                    case '(':
                        nestCount++;
                        break;
                    default:
                        break;
                }
                sb.Append(c);
            }
            yield return sb.ToString();
        }
    }
}
